/*
 *******************************************************************************
 *
 * Copyright (C) 2019-2020 Dialog Semiconductor.
 * This computer program includes Confidential, Proprietary Information
 * of Dialog Semiconductor. All Rights Reserved.
 *
 *******************************************************************************
 */

package com.example.suota.suota.suota;

import static com.example.suota.suota.global.SuotaProfile.SuotaProtocolState.ENABLE_NOTIFICATIONS;
import static com.example.suota.suota.global.SuotaProfile.SuotaProtocolState.END_SIGNAL;
import static com.example.suota.suota.global.SuotaProfile.SuotaProtocolState.SEND_BLOCK;
import static com.example.suota.suota.global.SuotaProfile.SuotaProtocolState.SET_GPIO_MAP;
import static com.example.suota.suota.global.SuotaProfile.SuotaProtocolState.SET_MEMORY_DEVICE;
import static com.example.suota.suota.global.SuotaProfile.Uuid.SUOTA_GPIO_MAP_UUID;
import static com.example.suota.suota.global.SuotaProfile.Uuid.SUOTA_MEM_DEV_UUID;
import static com.example.suota.suota.global.SuotaProfile.Uuid.SUOTA_PATCH_DATA_UUID;
import static com.example.suota.suota.global.SuotaProfile.Uuid.SUOTA_PATCH_LEN_UUID;
import static com.example.suota.suota.global.SuotaProfile.Uuid.SUOTA_SERV_STATUS_UUID;

import android.annotation.SuppressLint;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.content.Context;
import android.os.Handler;
import android.os.PowerManager;
import android.util.Log;

import androidx.annotation.NonNull;

import com.example.suota.suota.global.SuotaLibConfig;
import com.example.suota.suota.global.SuotaLibLog;
import com.example.suota.suota.global.SuotaProfile;
import com.example.suota.suota.interfaces.callbacks.ISuotaManagerCallback;
import com.example.suota.suota.utils.MainThread;

import java.util.ArrayList;
import java.util.UUID;

class SuotaProtocol {
    private static final String TAG = "SuotaProtocol";

    private SuotaManager suotaManager;
    private SuotaManagerCallback suotaManagerCallback;
    private SuotaFile suotaFile;
    private SuotaProfile.SuotaProtocolState state;
    private PowerManager.WakeLock wakeLock;
    private Handler timeoutHandler;
    private Runnable timeout;

    private long startTime;
    private long elapsedTime;
    private long uploadStartTime;
    private long uploadElapsedTime;
    private long currentBlockStartTime;

    private boolean suotaRunning;
    private boolean memoryDeviceSent;
    private boolean endSignalSent;

    private int currentBlock;
    private SendChunkOperation lastChunk;

    private SpeedStatistics statistics;
    private CurrentSpeedThread currentSpeedThread;

    public SuotaProtocol(SuotaManager suotaManager) {
        this.suotaManager = suotaManager;
        suotaManagerCallback = suotaManager.getSuotaManagerCallback();
        if (SuotaLibConfig.CALCULATE_STATISTICS)
            statistics = new SpeedStatistics();
        if (SuotaLibConfig.UPLOAD_TIMEOUT > 0) {
            timeoutHandler = new Handler();
            timeout = new Runnable() {
                @Override
                public void run() {
                    Log.e(TAG, "Upload timeout");
                    onError(SuotaProfile.Errors.UPLOAD_TIMEOUT);
                }
            };
        }
        reset();
    }

    // region Package Private Methods

    long getStartTime() {
        return startTime;
    }

    long getElapsedTime() {
        return elapsedTime;
    }

    long getUploadStartTime() {
        return uploadStartTime;
    }

    long getUploadElapsedTime() {
        return uploadElapsedTime;
    }

    void start() {
        reset();
        suotaRunning = true;
        if (SuotaLibLog.PROTOCOL || SuotaLibConfig.NOTIFY_SUOTA_LOG) {
            String uploadSize = "Upload size: " + suotaFile.getUploadSize() + " bytes";
            String blockSize = "Block size: " + suotaFile.getBlockSize() + " bytes";
            String chunkSize = "Chunk size: " + suotaFile.getChunkSize() + " bytes";
            String totalBlocks = "Total blocks: " + suotaFile.getTotalBlocks();
            String totalChunks = "Total chunks: " + suotaFile.getTotalChunks();
            String chunksPerBlock = "Chunks per block: " + suotaFile.getChunksPerBlock();
            String firmwareCrc = String.format("Firmware CRC: %#04x", suotaFile.getCrc());
            try {
                Log.d(TAG, "SUOTA延时前");
                Thread.sleep(1000);
                Log.d(TAG, "SUOTA延时后");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                if (SuotaLibLog.PROTOCOL) {
                    Log.d(TAG, "Start SUOTA");
                    Log.d(TAG, uploadSize);
                    Log.d(TAG, blockSize);
                    Log.d(TAG, chunkSize);
                    Log.d(TAG, totalBlocks);
                    Log.d(TAG, totalChunks);
                    Log.d(TAG, chunksPerBlock);
                    Log.d(TAG, firmwareCrc);
                }
                if (SuotaLibConfig.NOTIFY_SUOTA_LOG) {
                    notifySuotaLog(state, ISuotaManagerCallback.SuotaLogType.INFO, uploadSize + "\n" + blockSize + "\n" + chunkSize + "\n"
                            + totalBlocks  + "\n" + totalChunks + "\n" + chunksPerBlock + "\n" + firmwareCrc + "\n");
                }
            }
            // if (SuotaLibLog.PROTOCOL) {
            //     Log.d(TAG, "Start SUOTA");
            //     Log.d(TAG, uploadSize);
            //     Log.d(TAG, blockSize);
            //     Log.d(TAG, chunkSize);
            //     Log.d(TAG, totalBlocks);
            //     Log.d(TAG, totalChunks);
            //     Log.d(TAG, chunksPerBlock);
            //     Log.d(TAG, firmwareCrc);
            // }
            // if (SuotaLibConfig.NOTIFY_SUOTA_LOG) {
            //     notifySuotaLog(state, ISuotaManagerCallback.SuotaLogType.INFO, uploadSize + "\n" + blockSize + "\n" + chunkSize + "\n"
            //             + totalBlocks  + "\n" + totalChunks + "\n" + chunksPerBlock + "\n" + firmwareCrc + "\n");
            // }
        }
        onPreExecute().execute();
    }

    boolean isRunning() {
        return suotaRunning;
    }

    void notifyForSendingChunk(SendChunkOperation sendChunk) {
        lastChunk = sendChunk;

        // Block notification timeout
        if (SuotaLibConfig.UPLOAD_TIMEOUT > 0) {
            if (sendChunk.isLastChunk())
                timeoutHandler.postDelayed(timeout, SuotaLibConfig.UPLOAD_TIMEOUT);
        }

        int block = sendChunk.getBlock();
        int chunk = sendChunk.getChunk();

        if (SuotaLibLog.CHUNK || SuotaLibConfig.NOTIFY_SUOTA_LOG_CHUNK && SuotaLibConfig.NOTIFY_SUOTA_LOG) {
            String msg = "Send block " + (block + 1) + ", chunk " + (chunk + 1) + " of " + suotaFile.getBlockChunks(block)
                    + " (" + sendChunk.getChunkCount() + " of " + suotaFile.getTotalChunks() + "), size " + sendChunk.getValue().length;
            if (SuotaLibLog.CHUNK)
                Log.d(TAG, msg);
            if (SuotaLibConfig.NOTIFY_SUOTA_LOG_CHUNK && SuotaLibConfig.NOTIFY_SUOTA_LOG)
                notifySuotaLog(state, ISuotaManagerCallback.SuotaLogType.CHUNK, msg);
        }

        if (SuotaLibConfig.CALCULATE_STATISTICS) {
            if (chunk == 0)
                currentBlockStartTime = System.currentTimeMillis();
        }
    }

    void notifyForSendingMemoryDevice() {
        memoryDeviceSent = true;
    }

    void notifyForSendingEndSignal() {
        endSignalSent = true;
    }

    void destroy() {
        Log.d(TAG, "Destroy");
        suotaRunning = false;

        if (currentSpeedThread != null && !currentSpeedThread.isInterrupted()) {
            currentSpeedThread.interrupt();
        }

        if (SuotaLibConfig.UPLOAD_TIMEOUT > 0) {
            timeoutHandler.removeCallbacksAndMessages(null);
        }

        if (SuotaLibConfig.ACQUIRE_WAKELOCK) {
            releaseWakeLock();
        }
    }

    double getUploadAvg() {
        return statistics != null ? statistics.getUploadAvg() : -1;
    }

    double getAvg() {
        return statistics != null ? statistics.getAvg() : -1;
    }

    double getMax() {
        return statistics != null ? statistics.getMax() : -1;
    }

    double getMin() {
        return statistics != null ? statistics.getMin() : -1;
    }

    // endregion

    // region Gatt Callback Wrappers

    void onCharacteristicChanged(@NonNull BluetoothGattCharacteristic characteristic, byte[] value) {
        if (!suotaRunning)
            return;

        if (SuotaLibConfig.PROTOCOL_DEBUG) {
            if (!characteristic.getUuid().equals(SUOTA_SERV_STATUS_UUID)) {
                Log.e(TAG, "Received unexpected notification: " + characteristic.getUuid());
                suotaProtocolError();
                return;
            }
        }

        final int status = value[0] & 0xff;
        if (SuotaLibLog.PROTOCOL)
            Log.d(TAG, String.format("Service status: %#04x (state=%s)", status, state));

        if (status == SuotaProfile.Constants.IMAGE_STARTED) {
            onImageStarted();
        } else if (status == SuotaProfile.Constants.SERVICE_STATUS_OK) {
            onStatusOk();
        } else {
            onError(status);
        }
    }

    void onCharacteristicWrite(BluetoothGattCharacteristic characteristic) {
        if (!suotaRunning)
            return;
        UUID uuid = characteristic.getUuid();

        if (SuotaLibConfig.PROTOCOL_DEBUG) {
            // Memory device write callback may come after the image started notification, in which case the state will be SET_GPIO_MAP, not SET_MEMORY_DEVICE.
            if (!uuid.equals(SUOTA_MEM_DEV_UUID) && !uuid.equals(SUOTA_GPIO_MAP_UUID) && !uuid.equals(SUOTA_PATCH_LEN_UUID) && !uuid.equals(SUOTA_PATCH_DATA_UUID)
                || uuid.equals(SUOTA_MEM_DEV_UUID) && state != SET_MEMORY_DEVICE && state != SET_GPIO_MAP && state != END_SIGNAL
                || (state == SET_MEMORY_DEVICE || state == END_SIGNAL) && !uuid.equals(SUOTA_MEM_DEV_UUID)
                || (state == SET_GPIO_MAP) ^ uuid.equals(SUOTA_GPIO_MAP_UUID) && (state != SET_GPIO_MAP || !uuid.equals(SUOTA_MEM_DEV_UUID))
                || (state == SEND_BLOCK) ^ (uuid.equals(SUOTA_PATCH_LEN_UUID) || uuid.equals(SUOTA_PATCH_DATA_UUID))) {
                Log.e(TAG, "Unexpected characteristic write on state " + state + ": " + uuid);
                suotaProtocolError();
                return;
            }
        }

        if (SuotaLibLog.PROTOCOL) {
            if (uuid.equals(SUOTA_MEM_DEV_UUID)) {
                if (state == SET_MEMORY_DEVICE || state == SET_GPIO_MAP)
                    Log.d(TAG, "Memory device set");
                else if (state == END_SIGNAL)
                    Log.d(TAG, "End signal sent");
            } else if (uuid.equals(SUOTA_PATCH_LEN_UUID)) {
                Log.d(TAG, "Patch length set");
            }
        }

        if (uuid.equals(SUOTA_GPIO_MAP_UUID)) {
            if (SuotaLibLog.PROTOCOL)
                Log.d(TAG, "GPIO map set");
            moveToNextState().onPreExecute().execute();
        } else if (uuid.equals(SUOTA_PATCH_DATA_UUID)) {
            if (SuotaLibLog.CHUNK)
                Log.d(TAG, "Patch data write, chunk " + lastChunk.getChunkCount());
            if (SuotaLibConfig.NOTIFY_CHUNK_SEND)
                notifyChunkSend();
        }
    }

    void onDescriptorWrite(BluetoothGattDescriptor descriptor) {
        if (!suotaRunning)
            return;

        if (SuotaLibConfig.PROTOCOL_DEBUG) {
            if (state != ENABLE_NOTIFICATIONS || !descriptor.getCharacteristic().getUuid().equals(SUOTA_SERV_STATUS_UUID) || !descriptor.getUuid().equals(SuotaProfile.Uuid.CLIENT_CONFIG_DESCRIPTOR)) {
                Log.e(TAG, "Unexpected descriptor write on state " + state + ": " + descriptor.getCharacteristic().getUuid() + " " + descriptor.getUuid());
                suotaProtocolError();
                return;
            }
        }

        if (SuotaLibLog.PROTOCOL)
            Log.d(TAG, "Status notifications enabled");

        moveToNextState().onPreExecute().execute();
    }

    // endregion

    // region Private Methods

    // region Execution Control Methods

    private SuotaProtocol moveToNextState() {
        if (SuotaLibConfig.PROTOCOL_DEBUG) {
            if (state == END_SIGNAL)
                suotaProtocolError();
        }

        onPostExecute();
        switch (state) {
            case ENABLE_NOTIFICATIONS:
                state = SET_MEMORY_DEVICE;
                break;
            case SET_MEMORY_DEVICE:
                state = SET_GPIO_MAP;
                break;
            case SET_GPIO_MAP:
                state = SEND_BLOCK;
                break;
            case SEND_BLOCK:
                if (suotaFile.isLastBlock(currentBlock))
                    state = END_SIGNAL;
                break;
        }
        return this;
    }

    private SuotaProtocol onPreExecute() {
        switch (state) {
            case SET_MEMORY_DEVICE:
                prepareSetMemoryDevice();
                break;
            case SEND_BLOCK:
                prepareSendBlock();
                break;
        }
        return this;
    }

    private void execute() {
        switch (state) {
            case ENABLE_NOTIFICATIONS:
                enableNotifications();
                break;
            case SET_MEMORY_DEVICE:
                setMemoryDevice();
                break;
            case SET_GPIO_MAP:
                setGpioMap();
                break;
            case SEND_BLOCK:
                sendBlock();
                break;
            case END_SIGNAL:
                sendEndSignal();
                break;
        }
    }

    private void onPostExecute() {
        if (state == SEND_BLOCK)
            onBlockSent();
    }

    // endregion

    private void reset() {
        suotaFile = suotaManager.getSuotaFile();
        suotaRunning = false;
        memoryDeviceSent = false;
        endSignalSent = false;
        currentBlock = -1;
        lastChunk = null;
        if (SuotaLibConfig.CALCULATE_STATISTICS)
            statistics.reset();
        state = ENABLE_NOTIFICATIONS;
    }

    private void onImageStarted() {
        if (state == SET_MEMORY_DEVICE && memoryDeviceSent) {
            if (SuotaLibConfig.UPLOAD_TIMEOUT > 0)
                timeoutHandler.removeCallbacks(timeout);
            memoryDeviceSent = false; // detect future faulty notification
            if (SuotaLibLog.PROTOCOL)
                Log.d(TAG, "Image started notification");
            moveToNextState().onPreExecute().execute();
        } else {
            suotaProtocolError();
        }
    }

    // Occurs when a block has completely sent and after end signal
    private void onStatusOk() {
        if (state == SEND_BLOCK) {
            if (SuotaLibConfig.UPLOAD_TIMEOUT > 0)
                timeoutHandler.removeCallbacks(timeout);

            if (lastChunk == null || !lastChunk.isLastChunk()) {
                suotaProtocolError();
                return;
            }
            lastChunk = null; // detect future faulty notification

            moveToNextState().onPreExecute().execute();
        } else if (state == END_SIGNAL && endSignalSent) {
            if (SuotaLibConfig.UPLOAD_TIMEOUT > 0)
                timeoutHandler.removeCallbacks(timeout);

            if (SuotaLibLog.PROTOCOL)
                Log.d(TAG, "End signal notification");

            endSignalSent = false; // detect future faulty notification
            onPostExecute();

            elapsedTime = System.currentTimeMillis() - startTime;
            if (SuotaLibLog.PROTOCOL || SuotaLibConfig.NOTIFY_SUOTA_LOG) {
                String msg = "Update completed in " + (elapsedTime / 1000.) + " seconds";
                if (SuotaLibLog.PROTOCOL)
                    Log.d(TAG, msg);
                if (SuotaLibConfig.NOTIFY_SUOTA_LOG)
                    notifySuotaLog(state, ISuotaManagerCallback.SuotaLogType.INFO, msg);
            }

            suotaRunning = false;
            suotaManager.onSuotaProtocolSuccess();
        } else {
            suotaProtocolError();
        }
    }

    private void onError(final int error) {
        if (SuotaLibConfig.UPLOAD_TIMEOUT > 0)
            timeoutHandler.removeCallbacks(timeout);
        final String msg = "Error: " + error + ", " + SuotaProfile.Errors.suotaErrorCodeList.get(error);
        Log.e(TAG, msg);
        suotaRunning = false;
        state = SuotaProfile.SuotaProtocolState.ERROR;
        MainThread.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                suotaManagerCallback.onFailure(error);
                if (SuotaLibConfig.NOTIFY_SUOTA_LOG)
                    suotaManagerCallback.onSuotaLog(SuotaProfile.SuotaProtocolState.ERROR, ISuotaManagerCallback.SuotaLogType.INFO, msg);
            }
        });
        suotaManager.destroy();
    }

    private void suotaProtocolError() {
        if (SuotaLibConfig.UPLOAD_TIMEOUT > 0)
            timeoutHandler.removeCallbacks(timeout);
        Log.e(TAG, "SUOTA protocol error");
        suotaRunning = false;
        state = SuotaProfile.SuotaProtocolState.ERROR;
        MainThread.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                suotaManagerCallback.onFailure(SuotaProfile.Errors.PROTOCOL_ERROR);
                if (SuotaLibConfig.NOTIFY_SUOTA_LOG)
                    suotaManagerCallback.onSuotaLog(SuotaProfile.SuotaProtocolState.ERROR, ISuotaManagerCallback.SuotaLogType.INFO, "SUOTA protocol error");
            }
        });
        suotaManager.destroy();
    }

    @SuppressLint("WakelockTimeout")
    private void acquireWakeLock() {
        PowerManager powerManager = (PowerManager) suotaManager.getContext().getSystemService(Context.POWER_SERVICE);
        if (powerManager == null)
            return;

        Log.d(TAG, "Acquire wake lock");
        wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "SUOTA:" + SuotaProtocol.class.getSimpleName());
        wakeLock.acquire();
    }

    private void releaseWakeLock() {
        if (wakeLock != null && wakeLock.isHeld()) {
            Log.d(TAG, "Release wake lock");
            wakeLock.release();
        }
    }

    private void notifySuotaLog(final SuotaProfile.SuotaProtocolState state, final ISuotaManagerCallback.SuotaLogType type, final String log) {
        MainThread.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                suotaManagerCallback.onSuotaLog(state, type, log);
            }
        });
    }

    private void notifyChunkSend() {
        final int block = lastChunk.getBlock();
        final int chunk = lastChunk.getChunk();
        final int chunkCount = lastChunk.getChunkCount();
        MainThread.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                suotaManagerCallback.onChunkSend(chunkCount, suotaFile.getTotalChunks(), chunk + 1, block + 1, suotaFile.getBlockChunks(block), suotaFile.getTotalBlocks());
            }
        });
    }

    private void notifyBlockSent() {
        final int block = currentBlock + 1;
        MainThread.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                suotaManagerCallback.onBlockSent(block, suotaFile.getTotalBlocks());
            }
        });
    }

    private void notifyUploadProgress() {
        final float block = currentBlock + 1;
        MainThread.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                suotaManagerCallback.onUploadProgress(block / suotaFile.getTotalBlocks() * 100);
            }
        });
    }

    // endregion

    // region Step 1: Enable Notifications

    @SuppressLint("MissingPermission")
    private void enableNotifications() {
        BluetoothGatt gatt = suotaManager.getGatt();
        if (gatt == null) {
            suotaManager.failureNullGatt();
            return;
        }

        if (SuotaLibLog.PROTOCOL)
            Log.d(TAG, "Enable status notifications");
        if (SuotaLibConfig.NOTIFY_SUOTA_LOG)
            notifySuotaLog(state, ISuotaManagerCallback.SuotaLogType.INFO, "Enable status notifications");
        gatt.setCharacteristicNotification(suotaManager.getServiceStatusCharacteristic(), true);
        suotaManager.enqueueGattOperation(new GattOperation(suotaManager.getServiceStatusClientConfigDescriptor(), BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE));
    }

    // endregion

    // region Step 2: Set Mem Dev Info

    private void prepareSetMemoryDevice() {
        startTime = System.currentTimeMillis();
        if (SuotaLibConfig.ACQUIRE_WAKELOCK) {
            acquireWakeLock();
        }
    }

    private void setMemoryDevice() {
        int memoryDevice = suotaManager.getMemoryDevice();
        if (SuotaLibLog.PROTOCOL || SuotaLibConfig.NOTIFY_SUOTA_LOG) {
            String msg = String.format("Set memory device: %#010x", memoryDevice);
            if (SuotaLibLog.PROTOCOL)
                Log.d(TAG, msg);
            if (SuotaLibConfig.NOTIFY_SUOTA_LOG)
                notifySuotaLog(state, ISuotaManagerCallback.SuotaLogType.INFO, msg);
        }

        // Image started notification timeout
        if (SuotaLibConfig.UPLOAD_TIMEOUT > 0)
            timeoutHandler.postDelayed(timeout, SuotaLibConfig.UPLOAD_TIMEOUT);

        suotaManager.enqueueGattOperation(new MemoryDeviceOperation(this, suotaManager.getMemDevCharacteristic(), memoryDevice));
    }

    // endregion

    // region Step 3: Set GPIO Map

    private void setGpioMap() {
        int gpioMap = suotaManager.getGpioMap();
        if (SuotaLibLog.PROTOCOL || SuotaLibConfig.NOTIFY_SUOTA_LOG) {
            String msg = String.format("Set GPIO map: %#010x", gpioMap);
            if (SuotaLibLog.PROTOCOL)
                Log.d(TAG, msg);
            if (SuotaLibConfig.NOTIFY_SUOTA_LOG)
                notifySuotaLog(state, ISuotaManagerCallback.SuotaLogType.INFO, msg);
        }
        suotaManager.enqueueGattOperation(new GattOperation(GattOperation.OperationType.WRITE, suotaManager.getGpioMapCharacteristic(), gpioMap));
    }

    // endregion

    // region Step 4: Send Block
    // This step includes both sending patch length when required and sending the actual block data

    private void prepareSendBlock() {
        currentBlock++;
        lastChunk = null;

        if (currentBlock == 0) {
            if (SuotaLibLog.PROTOCOL)
                Log.d(TAG, "Upload started");
            if (SuotaLibConfig.NOTIFY_SUOTA_LOG)
                notifySuotaLog(state, ISuotaManagerCallback.SuotaLogType.INFO, "Upload started");
            uploadStartTime = System.currentTimeMillis();

            if (SuotaLibConfig.CALCULATE_STATISTICS) {
                statistics.setUploadStartTime(uploadStartTime);
                currentSpeedThread = new CurrentSpeedThread();
                currentSpeedThread.start();
            }

            sendPatchLength(suotaFile.getBlockSize());
        } else if (suotaFile.isLastBlock(currentBlock) && suotaFile.isLastBlockShorter()) {
            sendPatchLength(suotaFile.getLastBlockSize());
        }
    }

    private void sendPatchLength(final int blockSize) {
        if (SuotaLibLog.PROTOCOL || SuotaLibConfig.NOTIFY_SUOTA_LOG) {
            String msg = "Set patch length: " + blockSize;
            if (SuotaLibLog.PROTOCOL)
                Log.d(TAG, msg);
            if (SuotaLibConfig.NOTIFY_SUOTA_LOG)
                notifySuotaLog(state, ISuotaManagerCallback.SuotaLogType.INFO, msg);
        }
        suotaManager.enqueueGattOperation(new GattOperation(suotaManager.getPatchLengthCharacteristic(), (short) blockSize));
    }

    private void sendBlock() {
        if (SuotaLibLog.BLOCK)
            Log.d(TAG, "Current block: " + (currentBlock + 1) + " of " + suotaFile.getTotalBlocks());

        byte[][] block = suotaFile.getBlock(currentBlock);
        int chunk = 0;
        int chunkCount = currentBlock * suotaFile.getChunksPerBlock() + 1;
        for (final byte[] chunkData : block) {
            if (SuotaLibLog.CHUNK)
                Log.d(TAG, "Queue block " + (currentBlock + 1) + ", chunk " + (chunk + 1));
            suotaManager.enqueueGattOperation(new SendChunkOperation(this, suotaManager.getPatchDataCharacteristic(),
                    chunkData, chunkCount, chunk, currentBlock, suotaFile.isLastChunk(currentBlock, chunk)));
            chunk++;
            chunkCount++;
        }
    }

    private void onBlockSent() {
        boolean lastBlock = suotaFile.isLastBlock(currentBlock);
        if (lastBlock)
            uploadElapsedTime = System.currentTimeMillis() - uploadStartTime;

        if (SuotaLibConfig.CALCULATE_STATISTICS) {
            double elapsed = (System.currentTimeMillis() - currentBlockStartTime) / 1000.;
            int size = suotaFile.getBlockSize(currentBlock);
            final double speed = size / elapsed;

            if (SuotaLibLog.BLOCK || SuotaLibConfig.NOTIFY_SUOTA_LOG_BLOCK && SuotaLibConfig.NOTIFY_SUOTA_LOG) {
                String msg = "Block sent: " + (currentBlock + 1) + ", " + elapsed  + " seconds, " + (int) speed + " B/s";
                if (SuotaLibLog.BLOCK)
                    Log.d(TAG, msg);
                if (SuotaLibConfig.NOTIFY_SUOTA_LOG_BLOCK && SuotaLibConfig.NOTIFY_SUOTA_LOG)
                    notifySuotaLog(state, ISuotaManagerCallback.SuotaLogType.BLOCK, msg);
            }

            currentSpeedThread.update(size);
            statistics.update(size, speed);
            final double max = statistics.getMax();
            final double min = statistics.getMin();
            final double avg = !lastBlock ? statistics.getUploadAvg() : suotaFile.getUploadSize() / (uploadElapsedTime / 1000.);
            MainThread.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    suotaManagerCallback.updateSpeedStatistics(speed, max, min, avg);
                }
            });
        } else if (SuotaLibLog.BLOCK || SuotaLibConfig.NOTIFY_SUOTA_LOG_BLOCK && SuotaLibConfig.NOTIFY_SUOTA_LOG) {
            String msg = "Block sent: " + (currentBlock + 1);
            if (SuotaLibLog.BLOCK)
                Log.d(TAG, msg);
            if (SuotaLibConfig.NOTIFY_SUOTA_LOG_BLOCK && SuotaLibConfig.NOTIFY_SUOTA_LOG)
                notifySuotaLog(state, ISuotaManagerCallback.SuotaLogType.BLOCK, msg);
        }

        if (SuotaLibConfig.NOTIFY_BLOCK_SENT)
            notifyBlockSent();

        if (SuotaLibConfig.NOTIFY_UPLOAD_PROGRESS)
            notifyUploadProgress();

        if (SuotaLibLog.PROTOCOL || SuotaLibConfig.NOTIFY_SUOTA_LOG) {
            if (lastBlock) {
                String msg = "Upload completed in " + (uploadElapsedTime / 1000.) + " seconds";
                if (SuotaLibLog.PROTOCOL)
                    Log.d(TAG, msg);
                if (SuotaLibConfig.NOTIFY_SUOTA_LOG)
                    notifySuotaLog(state, ISuotaManagerCallback.SuotaLogType.INFO, msg);
            }
        }
    }

    // endregion

    // region Step 5: End Signal

    private void sendEndSignal() {
        if (SuotaLibLog.PROTOCOL)
            Log.d(TAG, "Send SUOTA end signal");
        if (SuotaLibConfig.NOTIFY_SUOTA_LOG)
            notifySuotaLog(state, ISuotaManagerCallback.SuotaLogType.INFO, "Send end signal");

        // End signal notification timeout
        if (SuotaLibConfig.UPLOAD_TIMEOUT > 0)
            timeoutHandler.postDelayed(timeout, SuotaLibConfig.UPLOAD_TIMEOUT);

        suotaManager.enqueueGattOperation(new SendEndSignalOperation(this, suotaManager.getMemDevCharacteristic()));
    }

    // endregion

    private class CurrentSpeedThread extends Thread {

        private int bytesSent;

        synchronized void update(int size) {
            bytesSent += size;
        }

        @Override
        public void run() {
            while (!interrupted()) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    break;
                }
                synchronized (CurrentSpeedThread.this) {
                    final int current = bytesSent;
                    bytesSent = 0;
                    MainThread.runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            suotaManagerCallback.updateCurrentSpeed(current);
                        }
                    });
                }
            }
        }
    }

    private static class SpeedStatistics {

        private long uploadStartTime;
        private int bytesSent;
        private double uploadAvg;
        private ArrayList<Double> speeds = new ArrayList<>();
        private double sum;
        private double max;
        private double min;

        SpeedStatistics() {
            reset();
        }

        void reset() {
            bytesSent = 0;
            speeds.clear();
            sum = 0;
            max = Double.MIN_VALUE;
            min = Double.MAX_VALUE;
        }

        void setUploadStartTime(long uploadStartTime) {
            this.uploadStartTime = uploadStartTime;
        }

        void update(int size, double speed) {
            bytesSent += size;
            uploadAvg = bytesSent / ((System.currentTimeMillis() - uploadStartTime) / 1000.);
            speeds.add(speed);
            sum += speed;
            if (max < speed)
                max = speed;
            if (min > speed)
                min = speed;
        }

        int getBytesSent() {
            return bytesSent;
        }

        double getUploadAvg() {
            return uploadAvg;
        }

        double getAvg() {
            return sum / speeds.size();
        }

        double getMax() {
            return max;
        }

        double getMin() {
            return min;
        }
    }
}
